// Copyright (C) 2010 David Sugar, Tycho Softworks.
//
// This file is part of GNU uCommon C++.
//
// GNU uCommon C++ is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// GNU uCommon C++ is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with GNU uCommon C++.  If not, see <http://www.gnu.org/licenses/>.

#include <apennine-config.h>
#include <ucommon/ucommon.h>
#include <ucommon/secure.h>
#include <ucommon/export.h>
#include <apennine/namespace.h>
#include <apennine/xmlrpc.h>

namespace apennine {

static void xmlput(CharacterProtocol *cp, const char *text)
{
    while(*text) {
        if(EOF == cp->putchar(*(text++)))
            return;
    }
}

static void xmlformat(CharacterProtocol *cp, const char *fmt, ...)
{
    va_list args;
    char buf[1024];

    buf[0] = 0;
    va_start(args, fmt);
    vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);

    xmlput(cp, buf);
}

static void xmltext(CharacterProtocol *cp, const char *src)
{
    while(*src) {
        switch(*src) {
        case '&':
            xmlput(cp, "&amp;");
            ++src;
            break;
        case '<':
            xmlput(cp, "&lt;");
            ++src;
            break;
        case '>':
            xmlput(cp, "&gt;");
            ++src;
            break;
        case '\"':
            xmlput(cp, "&quot;");
            ++src;
            break;
         case '\'':
            xmlput(cp, "&apos;");
            ++src;
            break;
        default:
            if(cp->putchar(*(src++)) == EOF)
                return;
        }
    }
}

static void b64encode(CharacterProtocol *cp, const unsigned char *src, size_t size)
{
    static const unsigned char alphabet[65] =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    unsigned bits;

    while(size >= 3) {
        bits = (((unsigned)src[0])<<16) |
            (((unsigned)src[1])<<8) | ((unsigned)src[2]);

        src += 3;
        size -= 3;

        cp->putchar(alphabet[(bits >> 18) & 0x3f]);
        cp->putchar(alphabet[(bits >> 12) & 0x3f]);
        cp->putchar(alphabet[(bits >> 6) & 0x3f]);
        cp->putchar(alphabet[bits & 0x3f]);
    }

    if(!size)
        return;

    bits = ((unsigned)src[0])<<16;
    cp->putchar(alphabet[(bits >> 18) & 0x3f]);
    if (size == 1) {
        cp->putchar(alphabet[(bits >> 12) & 0x3f]);
        cp->putchar('=');
    }
    else {
        bits |= ((unsigned)src[1])<<8;
        cp->putchar(alphabet[(bits >> 12) & 0x3f]);
        cp->putchar(alphabet[(bits >> 6) & 0x3f]);
    }
    cp->putchar('=');
}

void XMLRPC::reply(const char *fmt, ...)
{
    rpcint_t iv;
    time_t tv;
    struct tm *dt;
    double dv;
    const unsigned char *xp;
    size_t xsize;
    const char *sv;
    const char *valtype = "unknown";
    const char *name;
    bool end_flag = false;  // end of param...
    bool map_flag = false;
    bool struct_flag = false;
    bool array_flag = false;
    va_list args;
    va_start(args, fmt);

    switch(*fmt) {
    case '^':
    case '(':
    case '[':
    case '<':
    case '{':
    case 's':
    case 'i':
    case 'd':
    case 't':
    case 'b':
    case 'x':
        reset();
        xmlformat(this,
            "<?xml version=\"1.0\"?>\r\n"
            "<methodResponse>\r\n"
            " <params><param>\r\n");
        break;
    case '!':
        array_flag = true;
        break;
    case '+':
        map_flag = true;
    case '.':
        struct_flag = true;
    }

    if(!*fmt)
        end_flag = true;

    while(*fmt && *fmt != '$' && !end_flag) {
        switch(*fmt)
        {
        case '[':
            xmlformat(this, " <value><array><data>\r\n");
        case ',':
            array_flag = true;
            break;
        case ']':
            array_flag = false;
            xmlformat(this, " </data></array></value>\r\n");
            end_flag = true;
            break;
        case '}':
        case '>':
            map_flag = struct_flag = false;
            xmlformat(this,
                " </struct></value></member>\r\n"
                " </struct></value>\r\n");
            end_flag = true;
            break;
        case ';':
        case ':':
            name = va_arg(args, const char *);
            xmlformat(this,
                " </struct></value></member>\r\n"
                " <member><name>%s<value><struct>\r\n", name);
            break;
        case '{':
        case '<':
            name = va_arg(args, const char *);
            xmlformat(this,
                " <value><struct>\r\n"
                " <member><name>%s</name><value><struct>\r\n", name);
            struct_flag = map_flag = true;
            break;
        case '(':
            struct_flag = true;
            xmlformat(this, " <value><struct>\r\n");
            break;
        case ')':
            struct_flag = false;
            xmlformat(this,
                " </struct></value>\r\n");
            break;
        case 's':
        case 'i':
        case 'b':
        case 'd':
        case 't':
        case 'x':
            switch(*fmt) {
            case 'd':
                valtype = "double";
                break;
            case 'b':
                valtype = "boolean";
                break;
            case 'i':
                valtype = "i4";
                break;
            case 's':
                valtype = "string";
                break;
            case 't':
                valtype = "dateTime.iso8601";
                break;
            case 'x':
                valtype = "base64";
            }
            if(struct_flag) {
                name = va_arg(args, const char *);
                xmlformat(this,
                    "  <member><name>%s</name>\r\n"
                    "   <value><%s>", name, valtype);
            }
            else
                xmlformat(this,
                    "  <value><%s>", valtype);

            switch(*fmt) {
            case 'x':
                xp = va_arg(args, const unsigned char *);
                xsize = va_arg(args, size_t);
                b64encode(this, xp, xsize);
                break;
            case 's':
                sv = va_arg(args, const char *);
                xmltext(this, sv);
                break;
            case 'd':
                dv = va_arg(args, double);
                xmlformat(this, "%f", dv);
                break;
            case 'i':
            case 'b':
                iv = va_arg(args, rpcint_t);
                if(*fmt == 'b' && iv)
                    iv = 1;
                xmlformat(this, "%ld", (long)iv);
                break;
            case 't':
                tv = va_arg(args, time_t);
                dt = localtime(&tv);
                if(dt->tm_year < 1800)
                    dt->tm_year += 1900;
                xmlformat(this, "%04d%02d%02dT%02d:%02d:%02d",
                    dt->tm_year, dt->tm_mon + 1, dt->tm_mday,
                    dt->tm_hour, dt->tm_min, dt->tm_sec);
                break;
            }
            if(struct_flag)
                xmlformat(this, "</%s></value></member>\r\n", valtype);
            else
                xmlformat(this, "</%s></value>\r\n", valtype);

            if(!struct_flag && !array_flag)
                end_flag = true;
        }
        ++fmt;
    }

    if(*fmt == '$' || end_flag)
        xmlformat(this,
            " </param></params>\r\n"
            "</methodResponse>\r\n");

    va_end(args);
}

void XMLRPC::success(bool value)
{
    if(value)
        reply("^b$", (rpcint_t)1);
    else
        reply("^b$", (rpcint_t)0);
}

void XMLRPC::success(rpcint_t value)
{
    reply("^i$", value);
}

void XMLRPC::success(double value)
{
    reply("^d$", value);
}

void XMLRPC::success(const char *value)
{
    reply("^s$", value);
}

void XMLRPC::success_time(time_t value)
{
    reply("^t$", value);
}

void XMLRPC::success(void)
{
    reset();
    xmlformat(this,
        "<?xml version=\"1.0\"?>\r\n"
        "<methodResponse><params></params></methodResponse>\r\n");
}

void XMLRPC::failure(int code, const char *string)
{
    reset();
    xmlformat(this,
        "<?xml version=\"1.0\"?>\r\n"
        "<methodResponse>\r\n"
        " <fault><value><struct>\r\n"
        "  <member><name>faultCode</name>\r\n"
        "   <value><int>%d</int></value></member>\r\n"
        "  <member><name>faultString</name>\r\n"
        "   <value><string>", code);
    xmltext(this, string);
    xmlformat(this,
        "</string></value></member>\r\n"
        " </struct></value></fault>\r\n"
        "</methodResponse>\r\n");
}

void XMLRPC::prep(const char *fmt, ...)
{
    rpcint_t iv;
    time_t tv;
    struct tm *dt;
    double dv;
    const char *sv;
    const char *valtype = "unknown";
    const char *name;
    const unsigned char *xp;
    size_t xsize;
    bool beg_flag = true;
    bool end_flag = false;  // end of param...
    bool map_flag = false;
    bool struct_flag = false;
    bool array_flag = false;
    va_list args;
    va_start(args, fmt);

    switch(*fmt) {
    case '.':
        beg_flag = false;
        struct_flag = true;
        ++fmt;
        break;
    case '+':
    case ';':
    case ':':
        beg_flag = false;
        map_flag = true;
        struct_flag = true;
        ++fmt;
        break;
    case '!':
    case ',':
        array_flag = true;
        beg_flag = false;
        ++fmt;
        break;
    case '^':
        reset();
        name = va_arg(args, const char *);
        xmlformat(this,
            "<?xml version=\"1.0\"?>\r\n"
            "<methodCall><methodName>%s</methodName><params>\r\n", name);
        ++fmt;
        break;
    case ']':
    case '}':
    case '>':
        beg_flag = false;
    }

    while(*fmt && *fmt != '$') {
        if(beg_flag) {
            xmlformat(this, "<param>\r\n");
            beg_flag = false;
        }
        switch(*fmt) {
        case '[':
            xmlformat(this, " <value><array><data>\r\n");
        case ',':
            array_flag = true;
            break;
        case ']':
            array_flag = false;
            xmlformat(this, " </data></array></value>\r\n");
            end_flag = true;
            break;
        case '}':
        case '>':
            map_flag = struct_flag = false;
            xmlformat(this,
                " </struct></value></member>\r\n"
                " </struct></value>\r\n");
            end_flag = true;
            break;
        case ';':
        case ':':
            name = va_arg(args, const char *);
            xmlformat(this,
                " </struct></value></member>\r\n"
                " <member><name>%s<value><struct>\r\n", name);
            break;
        case '{':
        case '<':
            name = va_arg(args, const char *);
            xmlformat(this,
                " <value><struct>\r\n"
                " <member><name>%s</name><value><struct>\r\n", name);
            struct_flag = map_flag = true;
            break;
        case '(':
            struct_flag = true;
            xmlformat(this, " <value><struct>\r\n");
            break;
        case ')':
            struct_flag = false;
            xmlformat(this, " </struct></value>\r\n");
            break;
        case 's':
        case 'i':
        case 'b':
        case 'd':
        case 't':
        case 'x':
            switch(*fmt) {
            case 'd':
                valtype = "double";
                break;
            case 'b':
                valtype = "boolean";
                break;
            case 'i':
                valtype = "i4";
                break;
            case 's':
                valtype = "string";
                break;
            case 't':
                valtype = "dateTime.iso8601";
                break;
            case 'x':
                valtype = "base64";
                break;
            }
            if(struct_flag) {
                name = va_arg(args, const char *);
                xmlformat(this,
                    "  <member><name>%s</name>\r\n"
                    "   <value><%s>", name, valtype);
            }
            else
                xmlformat(this, "  <value><%s>", valtype);

            switch(*fmt) {
            case 's':
                sv = va_arg(args, const char *);
                xmltext(this, sv);
                break;
            case 'd':
                dv = va_arg(args, double);
                xmlformat(this, "%f", dv);
                break;
            case 'i':
            case 'b':
                iv = va_arg(args, rpcint_t);
                if(*fmt == 'b' && iv)
                    iv = 1;
                xmlformat(this, "%ld", (long)iv);
                break;
            case 't':
                tv = va_arg(args, time_t);
                dt = localtime(&tv);
                if(dt->tm_year < 1800)
                    dt->tm_year += 1900;
                xmlformat(this, "%04d%02d%02dT%02d:%02d:%02d",
                    dt->tm_year, dt->tm_mon + 1, dt->tm_mday,
                    dt->tm_hour, dt->tm_min, dt->tm_sec);
                break;
            case 'x':
                xp = va_arg(args, const unsigned char *);
                xsize = va_arg(args, size_t);
                b64encode(this, xp, xsize);
                break;
            }
            if(struct_flag)
                xmlformat(this, "</%s></value></member>\r\n", valtype);
            else
                xmlformat(this, "</%s></value>\r\n", valtype);

            if(!struct_flag && !array_flag)
                end_flag = true;
        }
        if(end_flag) {
            xmlformat(this, "</param>\r\n");
            end_flag = false;
            beg_flag = true;
        }
        ++fmt;
    }

    if(*fmt == '$')
        xmlformat(this, "</params></methodCall>\r\n");

    va_end(args);
}

} // end namespace

